/*
 * Copyright 2008-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baturu.mybatisminus.injector.parser;

import com.baturu.mybatisminus.model.TableInfo;
import com.baturu.mybatisminus.enums.QueryType;
import lombok.Getter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 参考Spring Data JPA解析方式
 */
@Getter
public class PartTree {

    private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
    private static final String QUERY_PATTERN = "find|query";
    private static final String COUNT_PATTERN = "count";
    private static final String UPDATE_PATTERN = "update";
    private static final String DELETE_PATTERN = "delete";
    private static final Pattern PREFIX_TEMPLATE = Pattern.compile(
            "^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + UPDATE_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By");

    private final Subject subject;

    private final Predicate predicate;

    public PartTree(String source, TableInfo tableInfo) {

        Assert.notNull(source, "Source must not be null");
        Assert.notNull(tableInfo, "TableInfo must not be null");

        Matcher matcher = PREFIX_TEMPLATE.matcher(source);

        if (!matcher.find()) {//找不到相当于默认findBy
            this.subject = new Subject(null, tableInfo);
            this.predicate = new Predicate(source, tableInfo);
        } else {
            this.subject = new Subject(matcher.group(0), tableInfo);
            this.predicate = new Predicate(source.substring(matcher.group(0).length()), tableInfo);
        }
    }


    public int getRequireArguments() {
        return subject.getRequireArguments() + predicate.getRequireArguments();
    }


    @Override
    public String toString() {

        return String.format("%s %s", StringUtils.collectionToDelimitedString(predicate.nodes, " or "),
                predicate.getOrderBySource() == null ? "" : predicate.getOrderBySource().toString()).trim();
    }


    private static String[] split(String text, String keyword) {

        Pattern pattern = Pattern.compile(String.format(KEYWORD_TEMPLATE, keyword));
        return pattern.split(text);
    }


    public static class OrPart {

        @Getter
        private final List<Part> parts;

        OrPart(String source, TableInfo tableInfo) {

            String[] split = split(source, "And");

            this.parts = Arrays.stream(split)
                    .filter(StringUtils::hasText)
                    .map(part -> new Part(part, tableInfo))
                    .collect(Collectors.toList());
        }


        public int getRequireArguments() {
            return parts.stream().map(Part::getRequireArguments).reduce(0, Integer::sum);
        }

        @Override
        public String toString() {
            return StringUtils.collectionToDelimitedString(parts, " and ");
        }
    }


    @Getter
    static class Subject {

        private static final Pattern FIND_BY_TEMPLATE = Pattern.compile("^(find|query)(\\p{Lu}.*)?By");//可有可没有
        private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*)?By");//可有可没有
        private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^deleteBy");//必须没有
        private static final Pattern UPDATE_BY_TEMPLATE = Pattern.compile("^update(\\p{Lu}.*)+By");//肯定有

        private List<Part> parts;

        private QueryType queryType = QueryType.SELECT;

        public Subject(String subject, TableInfo tableInfo) {
            if (StringUtils.isEmpty(subject)) {
                return;
            }
            Matcher findMatcher = FIND_BY_TEMPLATE.matcher(subject);
            if (findMatcher.matches()) {
                queryType = QueryType.SELECT;
                parts = splitNodes(findMatcher.group(2), tableInfo);
                return;
            }
            Matcher updateMatcher = UPDATE_BY_TEMPLATE.matcher(subject);
            if (updateMatcher.matches()) {
                queryType = QueryType.UPDATE;
                parts = splitNodes(updateMatcher.group(1), tableInfo);
                return;
            }
            Matcher countMatcher = COUNT_BY_TEMPLATE.matcher(subject);
            if (countMatcher.matches()) {
                queryType = QueryType.COUNT;
                parts = splitNodes(countMatcher.group(1), tableInfo);
                return;
            }
            Matcher deleteMatcher = DELETE_BY_TEMPLATE.matcher(subject);
            if (deleteMatcher.matches()) {
                queryType = QueryType.DELETE;
            }
        }

        public int getRequireArguments() {
            if (queryType.equals(QueryType.UPDATE)) {
                return parts.size();
            }
            return 0;
        }

        private List<Part> splitNodes(String text, TableInfo tableInfo) {
            if (StringUtils.isEmpty(text)) {
                return null;
            }
            String[] parts = split(text, "And");
            return Arrays.stream(parts)
                    .filter(StringUtils::hasText)
                    .map(part -> new Part(part, tableInfo))
                    .collect(Collectors.toList());
        }
    }


    @Getter
    static class Predicate {

        private static final String ORDER_BY = "OrderBy";

        private final List<OrPart> nodes;

        private final OrderBySource orderBySource;

        public Predicate(String predicate, TableInfo tableInfo) {

            String[] parts = split(predicate, ORDER_BY);

            if (parts.length > 2) {
                throw new IllegalArgumentException("OrderBy must not be used more than once in a method name!");
            }

            this.nodes = Arrays.stream(split(parts[0], "Or"))
                    .filter(StringUtils::hasText)
                    .map(part -> new OrPart(part, tableInfo))
                    .collect(Collectors.toList());

            this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1], tableInfo) : null;
        }

        public int getRequireArguments() {
            return nodes.stream().map(OrPart::getRequireArguments).reduce(0, Integer::sum);
        }

    }


}
